package com.termux.tasker.utils;

// Constants and functions for Tasker *extensions* to the plugin protocol
// See Also: http://tasker.dinglisch.net/plugins.html

// Release Notes

// v1.1 20140202
// added function variableNameValid()
// fixed some javadoc entries (thanks to David Stone)

// v1.2 20140211
// added ACTION_EDIT_EVENT

// v1.3 20140227
// added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER
// requestTimeoutMS(): added range check

// v1.4 20140516
// support for data pass through in REQUEST_QUERY intent
// some javadoc entries fixed (thanks again David :-))

// v1.5 20141120
// added RESULT_CODE_FAILED_PLUGIN_FIRST
// added Setting.VARNAME_ERROR_MESSAGE

// v1.6 20150213
// added Setting.getHintTimeoutMS()
// added Host.addHintTimeoutMS()

// v1.7 20160619
// null check for getCallingActivity() in hostSupportsOnFireVariableReplacement( Activity editActivity )

// v1.8 20161002
// added hostSupportsKeyEncoding(), setKeyEncoding() and Host.getKeysWithEncoding()

import java.net.URISyntaxException;
import java.security.SecureRandom;
import java.util.regex.Pattern;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.util.Log;

public class TaskerPlugin {

    private final static String 	TAG = "TaskerPlugin";

    private final static String 	BASE_KEY = "net.dinglisch.android.tasker";

    private final static String 	EXTRAS_PREFIX = BASE_KEY + ".extras.";

    private final static int		FIRST_ON_FIRE_VARIABLES_TASKER_VERSION = 80;

    public final static String		VARIABLE_PREFIX = "%";

    // when generating non-repeating integers, look this far back for repeats
    // see getPositiveNonRepeatingRandomInteger()
    private final static int		RANDOM_HISTORY_SIZE = 100;

    /**
     * 	Action that the EditActivity for an event plugin should be launched by
     */
    public final static String 		ACTION_EDIT_EVENT = BASE_KEY + ".ACTION_EDIT_EVENT";

    private final static String		VARIABLE_NAME_START_EXPRESSION =  "[\\w&&[^_]]";
    private final static String		VARIABLE_NAME_MID_EXPRESSION =  "[\\w0-9]+";
    private final static String		VARIABLE_NAME_END_EXPRESSION =  "[\\w0-9&&[^_]]";

    public final static String		VARIABLE_NAME_MAIN_PART_MATCH_EXPRESSION =
            VARIABLE_NAME_START_EXPRESSION + VARIABLE_NAME_MID_EXPRESSION + VARIABLE_NAME_END_EXPRESSION
            ;

    public final static String		VARIABLE_NAME_MATCH_EXPRESSION =
            VARIABLE_PREFIX + "+" +
                    VARIABLE_NAME_MAIN_PART_MATCH_EXPRESSION
            ;

    private static Pattern			VARIABLE_NAME_MATCH_PATTERN = null;

    /**
     *	@see #addVariableBundle(Bundle, Bundle)
     *	@see Host#getVariablesBundle(Bundle)
     */
    private final static String		EXTRA_VARIABLES_BUNDLE = EXTRAS_PREFIX + "VARIABLES";

    /**
     * 	Host capabilities, passed to plugin with edit intents
     */
    private final static String		EXTRA_HOST_CAPABILITIES = EXTRAS_PREFIX + "HOST_CAPABILITIES";

    /**
     *  @see Setting#hostSupportsVariableReturn(Bundle)
     */
    public final static int			EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES = 2;

    /**
     *	@see Condition#hostSupportsVariableReturn(Bundle)
     */
    public final static int			EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES = 4;

    /**
     * 	@see Setting#hostSupportsOnFireVariableReplacement(Bundle)
     */
    public final static int			EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT = 8;

    /**
     * @see Setting#hostSupportsVariableReturn(Bundle)
     */
    private final static int		EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES = 16;

    public final static int			EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION = 32;

    public final static int			EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH = 64;

    public final static int			EXTRA_HOST_CAPABILITY_ENCODING_JSON = 128;

    public final static int			EXTRA_HOST_CAPABILITY_ALL =
            EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES |
                    EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES |
                    EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT |
                    EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES|
                    EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION |
                    EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH |
                    EXTRA_HOST_CAPABILITY_ENCODING_JSON
            ;

    /**
     * Possible encodings of text in bundle values
     *
     * @see #setKeyEncoding(Bundle,String[],Encoding)
     */
    public enum Encoding { JSON }

    private final static String		BUNDLE_KEY_ENCODING_JSON_KEYS = BASE_KEY + ".JSON_ENCODED_KEYS";

    public static boolean hostSupportsKeyEncoding( Bundle extrasFromHost, Encoding encoding ) {
        switch ( encoding ) {
            case JSON:
                return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_ENCODING_JSON );
            default:
                return false;
        }
    }

    /**
     *
     *  Miscellaneous operational hints going one way or the other
     *  @see Setting#hostSupportsVariableReturn(Bundle)
     */

    private final static String		EXTRA_HINTS_BUNDLE = EXTRAS_PREFIX + "HINTS";

    private final static String		BUNDLE_KEY_HINT_PREFIX = ".hints.";

    private final static String		BUNDLE_KEY_HINT_TIMEOUT_MS = BUNDLE_KEY_HINT_PREFIX + "TIMEOUT";

    /**
     *
     *	@see #hostSupportsRelevantVariables(Bundle)
     *  @see #addRelevantVariableList(Intent, String[])
     *  @see #getRelevantVariableList(Bundle)
     */
    private final static String	BUNDLE_KEY_RELEVANT_VARIABLES = BASE_KEY + ".RELEVANT_VARIABLES";


    public static boolean hostSupportsRelevantVariables( Bundle extrasFromHost ) {
        return hostSupports( extrasFromHost,  EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES );
    }

    /**
     * Specifies to host which variables might be used by the plugin.
     *
     * Used in EditActivity, before setResult().
     *
     * @param  intentToHost the intent being returned to the host
     * @param  variableNames array of relevant variable names
     */
    public static void addRelevantVariableList( Intent intentToHost, String [] variableNames ) {
        intentToHost.putExtra( BUNDLE_KEY_RELEVANT_VARIABLES, variableNames );
    }

    /**
     * Validate a variable name.
     *
     * The basic requirement for variables from a plugin is that they must be all lower-case.
     *
     * @param  varName name to check
     */
    public static boolean variableNameValid( String varName ) {

        boolean validFlag = false;

        if ( varName == null )
            Log.d( TAG, "variableNameValid: null name" );
        else {
            if ( VARIABLE_NAME_MATCH_PATTERN == null )
                VARIABLE_NAME_MATCH_PATTERN = Pattern.compile( VARIABLE_NAME_MATCH_EXPRESSION, 0 );

            if ( VARIABLE_NAME_MATCH_PATTERN.matcher( varName ).matches() ) {

                if ( variableNameIsLocal( varName ) )
                    validFlag = true;
                else
                    Log.d( TAG, "variableNameValid: name not local: " + varName );
            }
            else
                Log.d( TAG, "variableNameValid: invalid name: " + varName );
        }

        return validFlag;
    }

    /**
     * Allows the plugin/host to indicate to each other a set of variables which they are referencing.
     * The host may use this to e.g. show a variable selection list in it's UI.
     * The host should use this if it previously indicated to the plugin that it supports relevant vars
     *
     * @param  fromHostIntentExtras usually from getIntent().getExtras()
     * @return variableNames an array of relevant variable names
     */
    public static String [] getRelevantVariableList( Bundle fromHostIntentExtras ) {

        String [] relevantVars = (String []) getBundleValueSafe( fromHostIntentExtras, BUNDLE_KEY_RELEVANT_VARIABLES, String [].class, "getRelevantVariableList" );

        if ( relevantVars == null )
            relevantVars = new String [0];

        return relevantVars;
    }

    /**
     * Used by: plugin QueryReceiver, FireReceiver
     *
     * Add a bundle of variable name/value pairs.
     *
     * Names must be valid Tasker local variable names.
     * Values must be String, String [] or ArrayList<String>
     * Null values cause deletion of possible already-existing variables
     * A null value where the variable does not already exist results in attempted deletion
     * of any existing array indices (%arr1, %arr2 etc)
     *
     * @param resultExtras the result extras from the receiver onReceive (from a call to getResultExtras())
     * @param variables the variables to send
     * @see Setting#hostSupportsVariableReturn(Bundle)
     * @see #variableNameValid(String)
     */
    public static void addVariableBundle( Bundle resultExtras, Bundle variables ) {
        resultExtras.putBundle( EXTRA_VARIABLES_BUNDLE, variables );
    }

    /**
     * Used by: plugin EditActivity
     *
     * Specify the encoding for a set of bundle keys.
     *
     * This is completely optional and currently only necessary if using Setting#setVariableReplaceKeys
     * where the corresponding values of some of the keys specified are JSON encoded.
     *
     * @param  resultBundleToHost the bundle being returned to the host
     * @param keys the keys being returned to the host which are encoded in some way
     * @param encoding the encoding of the values corresponding to the specified keys
     * @see #setVariableReplaceKeys(Bundle, String[])
     * @see #hostSupportsKeyEncoding(Bundle, Encoding)
     */
    public static void setKeyEncoding( Bundle resultBundleToHost, String [] keys, Encoding encoding ) {
        if ( Encoding.JSON.equals( encoding ) )
            addStringArrayToBundleAsString(
                    keys, resultBundleToHost, BUNDLE_KEY_ENCODING_JSON_KEYS, "setValueEncoding"
            );
        else
            Log.e( TAG, "unknown encoding: " + encoding );
    }

    // ----------------------------- SETTING PLUGIN ONLY --------------------------------- //

    public static class Setting {

        /**
         * 	Variable name into which a description of any error that occurred can be placed
         *  for the user to process.
         *
         *  Should *only* be set when the BroadcastReceiver result code indicates a failure.
         *
         *  Note that the user needs to have configured the task to continue after failure of the plugin
         *  action otherwise they will not be able to make use of the error message.
         *
         *  For use with #addRelevantVariableList(Intent, String[]) and #addVariableBundle(Bundle, Bundle)
         *
         */
        public final static String		VARNAME_ERROR_MESSAGE = VARIABLE_PREFIX + "errmsg";

        /**
         *	@see #setVariableReplaceKeys(Bundle, String[])
         */
        private final static String		BUNDLE_KEY_VARIABLE_REPLACE_STRINGS = EXTRAS_PREFIX + "VARIABLE_REPLACE_KEYS";

        /**
         *	@see #requestTimeoutMS(Intent, int)
         */
        private final static String 	EXTRA_REQUESTED_TIMEOUT = EXTRAS_PREFIX + "REQUESTED_TIMEOUT";

        /**
         *	@see #requestTimeoutMS(Intent, int)
         */

        public final static int 		REQUESTED_TIMEOUT_MS_NONE = 0;

        /**
         *	@see #requestTimeoutMS(Intent, int)
         */

        public final static int 		REQUESTED_TIMEOUT_MS_MAX = 3599000;

        /**
         *	@see #requestTimeoutMS(Intent, int)
         */

        public final static int 		REQUESTED_TIMEOUT_MS_NEVER = REQUESTED_TIMEOUT_MS_MAX + 1000;

        /**
         *  @see #signalFinish(Context, Intent, int, Bundle)
         *  @see #addCompletionIntent(Intent, Intent, ComponentName, boolean)
         */
        private final static String 	EXTRA_PLUGIN_COMPLETION_INTENT = EXTRAS_PREFIX + "COMPLETION_INTENT";

        /**
         *  @see #signalFinish(Context, Intent, int, Bundle)
         *  @see Host#getSettingResultCode(Intent)
         */
        public final static String 		EXTRA_RESULT_CODE = EXTRAS_PREFIX + "RESULT_CODE";

        /**
         *
         *	@see #signalFinish(Context, Intent, int, Bundle)
         *  @see #addCompletionIntent(Intent, Intent, ComponentName, boolean)
         */
        public final static String EXTRA_CALL_SERVICE_PACKAGE = BASE_KEY + ".EXTRA_CALL_SERVICE_PACKAGE";
        public final static String EXTRA_CALL_SERVICE = BASE_KEY + ".EXTRA_CALL_SERVICE";
        public final static String EXTRA_CALL_SERVICE_FOREGROUND = BASE_KEY + ".EXTRA_CALL_SERVICE_FOREGROUND";
        /**
         *  @see #signalFinish(Context, Intent, int, Bundle)
         *  @see Host#getSettingResultCode(Intent)
         */

        public final static int	RESULT_CODE_OK = Activity.RESULT_OK;
        public final static int	RESULT_CODE_OK_MINOR_FAILURES = Activity.RESULT_FIRST_USER;
        public final static int	RESULT_CODE_FAILED = Activity.RESULT_FIRST_USER + 1;
        public final static int	RESULT_CODE_PENDING = Activity.RESULT_FIRST_USER + 2;
        public final static int	RESULT_CODE_UNKNOWN = Activity.RESULT_FIRST_USER + 3;

        /**
         * If a plugin wants to define it's own error codes, start numbering them here.
         * The code will be placed in an error variable (%err in the case of Tasker) for
         * the user to process after the plugin action.
         */

        public final static int	RESULT_CODE_FAILED_PLUGIN_FIRST = Activity.RESULT_FIRST_USER + 9;

        /**
         * Used by: plugin EditActivity.
         *
         * Indicates to plugin that host will replace variables in specified bundle keys.
         *
         * Replacement takes place every time the setting is fired, before the bundle is
         * passed to the plugin FireReceiver.
         *
         * @param  extrasFromHost intent extras from the intent received by the edit activity
         * @see #setVariableReplaceKeys(Bundle, String[])
         */
        public static boolean hostSupportsOnFireVariableReplacement( Bundle extrasFromHost ) {
            return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT );
        }

        /**
         * Used by: plugin EditActivity.
         *
         * Description as above.
         *
         * This version also includes backwards compatibility with pre 4.2 Tasker versions.
         * At some point this function will be deprecated.
         *
         * @param  editActivity the plugin edit activity, needed to test calling Tasker version
         * @see #setVariableReplaceKeys(Bundle, String[])
         */

        public static boolean hostSupportsOnFireVariableReplacement( Activity editActivity ) {

            boolean supportedFlag = hostSupportsOnFireVariableReplacement( editActivity.getIntent().getExtras() );

            if ( ! supportedFlag ) {

                ComponentName callingActivity = editActivity.getCallingActivity();

                if ( callingActivity == null )
                    Log.w( TAG, "hostSupportsOnFireVariableReplacement: null callingActivity, defaulting to false" );
                else {
                    String callerPackage = callingActivity.getPackageName();

                    // Tasker only supporteed this from 1.0.10
                    supportedFlag =
                            ( callerPackage.startsWith( BASE_KEY ) ) &&
                                    ( getPackageVersionCode( editActivity.getPackageManager(), callerPackage ) > FIRST_ON_FIRE_VARIABLES_TASKER_VERSION )
                    ;
                }
            }

            return supportedFlag;
        }

        public static boolean hostSupportsSynchronousExecution( Bundle extrasFromHost ) {
            return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION );
        }

        /**
         * Request the host to wait the specified number of milliseconds before continuing.
         * Note that the host may choose to ignore the request.
         *
         * Maximum value is REQUESTED_TIMEOUT_MS_MAX.
         * Also available are REQUESTED_TIMEOUT_MS_NONE (continue immediately without waiting
         * for the plugin to finish) and REQUESTED_TIMEOUT_MS_NEVER (wait forever for
         * a result).
         *
         * Used in EditActivity, before setResult().
         *
         * @param  intentToHost the intent being returned to the host
         * @param  timeoutMS
         */
        public static void requestTimeoutMS( Intent intentToHost, int timeoutMS ) {
            if ( timeoutMS < 0 )
                Log.w( TAG, "requestTimeoutMS: ignoring negative timeout (" + timeoutMS + ")" );
            else {
                if (
                        ( timeoutMS > REQUESTED_TIMEOUT_MS_MAX ) &&
                                ( timeoutMS != REQUESTED_TIMEOUT_MS_NEVER )
                        ) {
                    Log.w( TAG, "requestTimeoutMS: requested timeout " + timeoutMS + " exceeds maximum, setting to max (" + REQUESTED_TIMEOUT_MS_MAX + ")" );
                    timeoutMS = REQUESTED_TIMEOUT_MS_MAX;
                }
                intentToHost.putExtra( EXTRA_REQUESTED_TIMEOUT, timeoutMS );
            }
        }

        /**
         * Used by: plugin EditActivity
         *
         * Indicates to host which bundle keys should be replaced.
         *
         * @param  resultBundleToHost the bundle being returned to the host
         * @param  listOfKeyNames which bundle keys to replace variables in when setting fires
         * @see #hostSupportsOnFireVariableReplacement(Bundle)
         * @see #setKeyEncoding(Bundle,String[],Encoding)
         */
        public static void setVariableReplaceKeys( Bundle resultBundleToHost, String [] listOfKeyNames ) {
            addStringArrayToBundleAsString(
                    listOfKeyNames, resultBundleToHost, BUNDLE_KEY_VARIABLE_REPLACE_STRINGS,
                    "setVariableReplaceKeys"
            );
        }

        /**
         * Used by: plugin FireReceiver
         *
         * Indicates to plugin whether the host will process variables which it passes back
         *
         * @param  extrasFromHost intent extras from the intent received by the FireReceiver
         * @see #signalFinish(Context, Intent, int, Bundle)
         */
        public static boolean hostSupportsVariableReturn( Bundle extrasFromHost ) {
            return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES );
        }

        /**
         * Used by: plugin FireReceiver
         *
         * Tell the host that the plugin has finished execution.
         *
         * This should only be used if RESULT_CODE_PENDING was returned by FireReceiver.onReceive().
         *
         * @param originalFireIntent the intent received from the host (via onReceive())
         * @param resultCode level of success in performing the settings
         * @param vars any variables that the plugin wants to set in the host
         * @see #hostSupportsSynchronousExecution(Bundle)
         */
        public static boolean signalFinish( Context context, Intent originalFireIntent, int resultCode, Bundle vars ) {

            String errorPrefix = "signalFinish: ";

            boolean okFlag = false;

            String completionIntentString = (String) getExtraValueSafe( originalFireIntent, Setting.EXTRA_PLUGIN_COMPLETION_INTENT, String.class, "signalFinish" );

            if ( completionIntentString != null ) {

                Uri completionIntentUri = null;
                try {
                    completionIntentUri = Uri.parse( completionIntentString );
                }
                // 	should only throw NullPointer but don't particularly trust it
                catch ( Exception e ) {
                    Log.w( TAG, errorPrefix + "couldn't parse " + completionIntentString );
                }

                if ( completionIntentUri != null ) {
                    try {
                        Intent completionIntent = Intent.parseUri( completionIntentString, Intent.URI_INTENT_SCHEME );

                        completionIntent.putExtra( EXTRA_RESULT_CODE, resultCode );

                        if ( vars != null )
                            completionIntent.putExtra( EXTRA_VARIABLES_BUNDLE, vars );

                        String callServicePackage = (String) getExtraValueSafe(completionIntent, Setting.EXTRA_CALL_SERVICE_PACKAGE, String.class, "signalFinish");
                        String callService = (String) getExtraValueSafe(completionIntent, Setting.EXTRA_CALL_SERVICE, String.class, "signalFinish");
                        Boolean foreground = (Boolean) getExtraValueSafe(completionIntent, Setting.EXTRA_CALL_SERVICE_FOREGROUND, Boolean.class, "signalFinish");
                        if (callServicePackage != null && callService != null && foreground != null) {
                            completionIntent.setComponent(new ComponentName(callServicePackage, callService));
                            if (foreground && android.os.Build.VERSION.SDK_INT >= 26) {
                                context.startForegroundService(completionIntent);
                            } else {
                                context.startService(completionIntent);
                            }
                        } else {
                            context.sendBroadcast(completionIntent);
                        }

                        okFlag = true;
                    }
                    catch ( URISyntaxException e ) {
                        Log.w( TAG, errorPrefix + "bad URI: " + completionIntentUri );
                    }
                }
            }

            return okFlag;
        }

        /**
         * Check for a hint on the timeout value the host is using.
         * Used by: plugin FireReceiver.
         * Requires Tasker 4.7+
         *
         * @param  extrasFromHost intent extras from the intent received by the FireReceiver
         * @return timeoutMS the hosts timeout setting for the action or -1 if no hint is available.
         *
         * @see #REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX, REQUESTED_TIMEOUT_MS_NEVER
         */
        public static int getHintTimeoutMS( Bundle extrasFromHost ) {

            int timeoutMS = -1;

            Bundle hintsBundle = (Bundle) TaskerPlugin.getBundleValueSafe( extrasFromHost, EXTRA_HINTS_BUNDLE, Bundle.class, "getHintTimeoutMS" );

            if ( hintsBundle != null ) {

                Integer val = (Integer) getBundleValueSafe( hintsBundle, BUNDLE_KEY_HINT_TIMEOUT_MS, Integer.class, "getHintTimeoutMS" );

                if ( val != null )
                    timeoutMS = val;
            }

            return timeoutMS;
        }
    }

    // ----------------------------- CONDITION/EVENT PLUGIN ONLY --------------------------------- //

    public static class Condition {

        /**
         * @see #getResultReceiver(Intent)
         */
        public final static String EXTRA_RESULT_RECEIVER = BASE_KEY + ".EXTRA_RESULT_RECEIVER";
        /**
         * Used by: plugin QueryReceiver
         *
         * Indicates to plugin whether the host will process variables which it passes back
         *
         * @param  extrasFromHost intent extras from the intent received by the QueryReceiver
         * @see #addVariableBundle(Bundle, Bundle)
         */
        public static boolean hostSupportsVariableReturn( Bundle extrasFromHost ) {
            return hostSupports( extrasFromHost,  EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES );
        }

        public static ResultReceiver getResultReceiver(Intent intentFromHost) {
            if (intentFromHost == null) {
                return null;
            }
            return (ResultReceiver) getExtraValueSafe(intentFromHost, EXTRA_RESULT_RECEIVER, ResultReceiver.class, "getResultReceiver");

        }
    }

    // ----------------------------- EVENT PLUGIN ONLY --------------------------------- //

    public static class Event {

        public final static String	PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY = BASE_KEY + ".MESSAGE_ID";

        private final static String	EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA = EXTRAS_PREFIX + "PASS_THROUGH_DATA";

        /**
         * @param  extrasFromHost intent extras from the intent received by the QueryReceiver
         * @see #addPassThroughData(Intent, Bundle)
         */
        public static boolean hostSupportsRequestQueryDataPassThrough( Bundle extrasFromHost ) {
            return hostSupports( extrasFromHost,  EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH );
        }

        /**
         * Specify a bundle of data (probably representing whatever change happened in the condition)
         * which will be included in the QUERY_CONDITION broadcast sent by the host for each
         * event instance of the plugin.
         *
         * The minimal purpose is to enable the plugin to associate a QUERY_CONDITION to the
         * with the REQUEST_QUERY that caused it.
         *
         * Note that for security reasons it is advisable to also store a message ID with the bundle
         * which can be compared to known IDs on receipt. The host cannot validate the source of
         * REQUEST_QUERY intents so fake data may be passed. Replay attacks are also possible.
         * addPassThroughMesssageID() can be used to add an ID if the plugin doesn't wish to add it's
         * own ID to the pass through bundle.
         *
         * Note also that there are several situations where REQUEST_QUERY will not result in a
         * QUERY_CONDITION intent (e.g. event throttling by the host), so plugin-local data
         * indexed with a message ID needs to be timestamped and eventually timed-out.
         *
         * This function can be called multiple times, each time all keys in data will be added to
         * that of previous calls.
         *
         * @param requestQueryIntent intent being sent to the host
         * @param data the data to be passed-through
         * @see #hostSupportsRequestQueryDataPassThrough(Bundle)
         * @see #retrievePassThroughData(Intent)
         * @see #addPassThroughMessageID
         *
         */
        public static void addPassThroughData( Intent requestQueryIntent, Bundle data ) {

            Bundle passThroughBundle = retrieveOrCreatePassThroughBundle( requestQueryIntent );

            passThroughBundle.putAll( data );
        }

        /**
         * Retrieve the pass through data from a QUERY_REQUEST from the host which was generated
         * by a REQUEST_QUERY from the plugin.
         *
         * Note that if addPassThroughMessageID() was previously called, the data will contain an extra
         * key TaskerPlugin.Event.PASS_THOUGH_BUNDLE_MESSAGE_ID_KEY.
         *
         * @param queryConditionIntent QUERY_REQUEST sent from host
         * @return data previously added to the REQUEST_QUERY intent
         * @see #hostSupportsRequestQueryDataPassThrough(Bundle)
         * @see #addPassThroughData(Intent,Bundle)
         */
        public static Bundle retrievePassThroughData( Intent queryConditionIntent ) {
            return (Bundle) getExtraValueSafe(
                    queryConditionIntent,
                    EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA,
                    Bundle.class,
                    "retrievePassThroughData"
            );
        }

        /**
         * Add a message ID to a REQUEST_QUERY intent which will then be included in the corresponding
         * QUERY_CONDITION broadcast sent by the host for each event instance of the plugin.
         *
         * The minimal purpose is to enable the plugin to associate a QUERY_CONDITION to the
         * with the REQUEST_QUERY that caused it. It also allows the message to be verified
         * by the plugin to prevent e.g. replay attacks
         *
         * @param requestQueryIntent intent being sent to the host
         * @return a guaranteed non-repeating within 100 calls message ID
         * @see #hostSupportsRequestQueryDataPassThrough(Bundle)
         * @see #retrievePassThroughData(Intent)
         * @return an ID for the bundle so it can be identified and the caller verified when it is again received by the plugin
         *
         */
        public static int addPassThroughMessageID( Intent requestQueryIntent ) {

            Bundle passThroughBundle = retrieveOrCreatePassThroughBundle( requestQueryIntent );

            int id = getPositiveNonRepeatingRandomInteger();

            passThroughBundle.putInt( PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY, id );

            return id;
        }

        /*
         * Retrieve the pass through data from a QUERY_REQUEST from the host which was generated
         * by a REQUEST_QUERY from the plugin.
         *
         * @param queryConditionIntent QUERY_REQUEST sent from host
         * @return the ID which was passed through by the host, or -1 if no ID was found
         * @see #hostSupportsRequestQueryDataPassThrough(Bundle)
         * @see #addPassThroughData(Intent,Bundle)
         */
        public static int retrievePassThroughMessageID( Intent queryConditionIntent ) {

            int toReturn = -1;

            Bundle passThroughData = Event.retrievePassThroughData( queryConditionIntent );

            if ( passThroughData != null ) {
                Integer id = (Integer) getBundleValueSafe(
                        passThroughData,
                        PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY,
                        Integer.class,
                        "retrievePassThroughMessageID"
                );

                if ( id != null )
                    toReturn = id;
            }

            return toReturn;
        }

        // internal use
        private static Bundle retrieveOrCreatePassThroughBundle( Intent requestQueryIntent ) {

            Bundle passThroughBundle;

            if ( requestQueryIntent.hasExtra( EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA ) )
                passThroughBundle = requestQueryIntent.getBundleExtra( EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA );
            else {
                passThroughBundle = new Bundle();
                requestQueryIntent.putExtra( EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA, passThroughBundle );
            }

            return passThroughBundle;
        }
    }
    // ---------------------------------- HOST  ----------------------------------------- //

    public static class Host {

        /**
         * Tell the plugin what capabilities the host support. This should be called when sending
         * intents to any EditActivity, FireReceiver or QueryReceiver.
         *
         * @param  toPlugin the intent we're sending
         * @return capabilities one or more of the EXTRA_HOST_CAPABILITY_XXX flags
         */
        public static Intent addCapabilities( Intent toPlugin, int capabilities ) {
            return toPlugin.putExtra( EXTRA_HOST_CAPABILITIES, capabilities  );
        }

        /**
         * Add an intent to the fire intent before it goes to the plugin FireReceiver, which the plugin
         * can use to signal when it is finished. Only use if @code{pluginWantsSychronousExecution} is true.
         *
         * @param fireIntent fire intent going to the plugin
         * @param completionIntent intent which will signal the host that the plugin is finished.
         * Implementation is host-dependent.
         */
        public static void addCompletionIntent(Intent fireIntent, Intent completionIntent, ComponentName callService, boolean foreground) {
            if (callService != null) {
                completionIntent.putExtra(Setting.EXTRA_CALL_SERVICE_PACKAGE, callService.getPackageName());
                completionIntent.putExtra(Setting.EXTRA_CALL_SERVICE, callService.getClassName());
                completionIntent.putExtra(Setting.EXTRA_CALL_SERVICE_FOREGROUND, foreground);
            }
            fireIntent.putExtra(
                    Setting.EXTRA_PLUGIN_COMPLETION_INTENT,
                    completionIntent.toUri(Intent.URI_INTENT_SCHEME)
            );
        }

        /**
         * When a setting plugin is finished, it sends the host the intent which was passed to it
         * via @code{addCompletionIntent}.
         *
         * @param completionIntent intent returned from the plugin when it finished.
         * @return resultCode measure of plugin success, defaults to UNKNOWN
         */
        public static int getSettingResultCode( Intent completionIntent ) {

            Integer val = (Integer) getExtraValueSafe( completionIntent, Setting.EXTRA_RESULT_CODE, Integer.class, "getSettingResultCode" );

            return ( val == null ) ? Setting.RESULT_CODE_UNKNOWN : val;
        }

        /**
         * Extract a bundle of variables from an intent received from the FireReceiver. This
         * should be called if the host previously indicated to the plugin
         * that it supports setting variable return.
         *
         * @param resultExtras getResultExtras() from BroadcastReceiver:onReceive()
         * @return variables a bundle of variable name/value pairs
         * @see #addCapabilities(Intent, int)
         */

        public static Bundle getVariablesBundle( Bundle resultExtras ) {
            return (Bundle) getBundleValueSafe(
                    resultExtras, EXTRA_VARIABLES_BUNDLE, Bundle.class, "getVariablesBundle"
            );
        }

        /**
         * Inform a setting plugin of the timeout value the host is using.
         *
         * @param toPlugin the intent we're sending
         * @param timeoutMS the hosts timeout setting for the action. Note that this may differ from
         * that which the plugin requests.
         * @see #REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX, REQUESTED_TIMEOUT_MS_NEVER
         */
        public static void addHintTimeoutMS( Intent toPlugin, int timeoutMS ) {
            getHintsBundle( toPlugin, "addHintTimeoutMS" ).putInt( BUNDLE_KEY_HINT_TIMEOUT_MS, timeoutMS );
        }

        private static Bundle getHintsBundle( Intent intent, String funcName ) {

            Bundle hintsBundle = (Bundle) getExtraValueSafe( intent, EXTRA_HINTS_BUNDLE, Bundle.class, funcName );

            if ( hintsBundle == null ) {
                hintsBundle = new Bundle();
                intent.putExtra( EXTRA_HINTS_BUNDLE, hintsBundle );
            }

            return hintsBundle;
        }

        public static boolean haveRequestedTimeout( Bundle extrasFromPluginEditActivity ) {
            return extrasFromPluginEditActivity.containsKey( Setting.EXTRA_REQUESTED_TIMEOUT );
        }

        public static int getRequestedTimeoutMS( Bundle extrasFromPluginEditActivity ) {
            return
                    (Integer) getBundleValueSafe(
                            extrasFromPluginEditActivity, Setting.EXTRA_REQUESTED_TIMEOUT,	Integer.class, "getRequestedTimeout"
                    )
                    ;
        }

        public static String [] getSettingVariableReplaceKeys( Bundle fromPluginEditActivity ) {
            return getStringArrayFromBundleString(
                    fromPluginEditActivity, Setting.BUNDLE_KEY_VARIABLE_REPLACE_STRINGS,
                    "getSettingVariableReplaceKeys"
            );
        }

        public static String [] getKeysWithEncoding( Bundle fromPluginEditActivity, Encoding encoding ) {

            String [] toReturn = null;

            if ( Encoding.JSON.equals( encoding ) )
                toReturn = getStringArrayFromBundleString(
                        fromPluginEditActivity, TaskerPlugin.BUNDLE_KEY_ENCODING_JSON_KEYS,
                        "getKeyEncoding:JSON"
                );
            else
                Log.w( TAG, "Host.getKeyEncoding: unknown encoding " + encoding );

            return toReturn;
        }

        public static boolean haveRelevantVariables( Bundle b ) {
            return b.containsKey( BUNDLE_KEY_RELEVANT_VARIABLES );
        }

        public static void cleanRelevantVariables( Bundle b ) {
            b.remove( BUNDLE_KEY_RELEVANT_VARIABLES );
        }

        public static void cleanHints( Bundle extras ) {
            extras.remove( TaskerPlugin.EXTRA_HINTS_BUNDLE );
        }

        public static void cleanRequestedTimeout( Bundle extras ) {
            extras.remove( Setting.EXTRA_REQUESTED_TIMEOUT );
        }

        public static void cleanSettingReplaceVariables( Bundle b ) {
            b.remove( Setting.BUNDLE_KEY_VARIABLE_REPLACE_STRINGS );
        }
    }

    // ---------------------------------- HELPER FUNCTIONS -------------------------------- //

    private static Object getBundleValueSafe( Bundle b, String key, Class<?> expectedClass, String funcName ) {
        Object value = null;

        if ( b != null ) {
            if ( b.containsKey( key ) ) {
                Object obj = b.get( key );
                if ( obj == null )
                    Log.w( TAG, funcName + ": " + key + ": null value" );
                else if ( obj.getClass() != expectedClass )
                    Log.w( TAG, funcName + ": " + key + ": expected " + expectedClass.getClass().getName() + ", got " + obj.getClass().getName() );
                else
                    value = obj;
            }
        }
        return value;
    }

    private static Object getExtraValueSafe( Intent i, String key, Class<?> expectedClass, String funcName ) {
        return ( i.hasExtra( key ) ) ?
                getBundleValueSafe( i.getExtras(), key, expectedClass, funcName ) :
                null;
    }

    private static boolean hostSupports( Bundle extrasFromHost, int capabilityFlag ) {
        Integer flags = (Integer) getBundleValueSafe( extrasFromHost, EXTRA_HOST_CAPABILITIES, Integer.class, "hostSupports" );
        return
                ( flags != null ) &&
                        ( ( flags & capabilityFlag ) > 0 )
                ;
    }

    public static int getPackageVersionCode( PackageManager pm, String packageName ) {

        int code = -1;

        if ( pm != null ) {
            try {
                PackageInfo pi = pm.getPackageInfo( packageName, 0 );
                if ( pi != null )
                    code = pi.versionCode;
            }
            catch ( Exception e ) {
                Log.e( TAG, "getPackageVersionCode: exception getting package info" );
            }
        }

        return code;
    }

    private static boolean variableNameIsLocal( String varName ) {

        int digitCount = 0;
        int length = varName.length();

        for ( int x = 0; x < length; x++ ) {
            char ch = varName.charAt( x );

            if ( Character.isUpperCase( ch ) )
                return false;
            else if ( Character.isDigit( ch ) )
                digitCount++;
        }

        if ( digitCount == ( varName.length() - 1 ) )
            return false;

        return true;
    }

    private static String [] getStringArrayFromBundleString( Bundle bundle, String key, String funcName ) {

        String spec = (String) getBundleValueSafe( bundle, key, String.class, funcName );

        String [] toReturn = null;

        if ( spec != null )
            toReturn = spec.split( " " );

        return toReturn;
    }

    private static void addStringArrayToBundleAsString( String [] toAdd, Bundle bundle, String key, String callerName ) {

        StringBuilder builder = new StringBuilder();

        if ( toAdd != null ) {

            for ( String keyName : toAdd ) {

                if ( keyName.contains( " " ) )
                    Log.w( TAG, callerName + ": ignoring bad keyName containing space: " + keyName );
                else {
                    if ( builder.length() > 0 )
                        builder.append( ' ' );

                    builder.append( keyName );
                }

                if ( builder.length() > 0 )
                    bundle.putString( key, builder.toString() );
            }
        }
    }

    // state tracking for random number sequence
    private static int [] 		lastRandomsSeen = null;
    private static int 			randomInsertPointer = 0;
    private static SecureRandom sr = null;

    /**
     * Generate a sequence of secure random positive integers which is guaranteed not to repeat
     * in the last 100 calls to this function.
     *
     * @return a random positive integer
     */
    public static int getPositiveNonRepeatingRandomInteger() {

        // initialize on first call
        if ( sr == null ) {
            sr = new SecureRandom();
            lastRandomsSeen = new int[RANDOM_HISTORY_SIZE];

            for ( int x = 0; x < lastRandomsSeen.length; x++ )
                lastRandomsSeen[x] = -1;
        }

        int toReturn;
        do {
            // pick a number
            toReturn = sr.nextInt( Integer.MAX_VALUE );

            // check we havn't see it recently
            for ( int seen : lastRandomsSeen ) {
                if ( seen == toReturn ) {
                    toReturn = -1;
                    break;
                }
            }
        }
        while ( toReturn == -1 );

        // update history
        lastRandomsSeen[randomInsertPointer] = toReturn;
        randomInsertPointer = ( randomInsertPointer + 1 ) % lastRandomsSeen.length;

        return toReturn;
    }

}